# pip install numpy xarray netCDF4 matplotlib plotly
# pip install "dask[complete]" -q
# pip install geopy ipywidgets folium -q
# pip install pycountry -q
# pip install shapely
# pip install -U kaleido
# !jupyter nbextension enable --py widgetsnbextension
# !jupyter labextension install @jupyter-widgets/jupyterlab-manager
from ipywidgets import Layout, Dropdown, widgets
from IPython.display import display, clear_output, IFrame
from functools import partial
import datetime
import numpy as np
import modules.n1_utilities as uti
import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning)
country_list = uti.read_json_to_sorted_dict('countries.json')
months = uti.read_json_to_dict('months.json')
timescales = uti.read_json_to_dict('timescales.json')
subset_area = None
index = None
bounding_box = (None, None, None, None)
active_btn = None
selected = {
"country": None,
"adm1_subarea": None,
"adm2_subarea": None,
"timescale": None,
"month": None,
"year": None,
"year_range": None
}
placeholders = {
"country": "no country selected...",
"adm1_subarea": "no adm1 subarea selected...",
"adm2_subarea": "no adm2 subarea selected...",
"timescale": "no timescale selected...",
"month": "no month selected...",
"year": "no year selected..."
}
uti.save_selection(placeholders)
# Custom style and layout for descriptions and dropdowns
style = {'description_width': '150px'}
dropdown_layout = Layout(width='400px', display='flex', justify_content='flex-end')
range_layout = Layout(width='400px')
btn_layout = Layout(width='400px')
# Dropdown for countries
country_names = [country['name'] for country in country_list]
country_selector = widgets.Dropdown(
options=[placeholders['country']] + country_names,
description='Select a country:',
style=style,
layout=dropdown_layout
)
# Dropdown for subareas, initially empty
adm1_subarea_selector = widgets.Dropdown(
options=[placeholders['adm1_subarea']],
description='a subarea of first level:',
style=style,
layout=dropdown_layout
)
adm2_subarea_selector = widgets.Dropdown(
options=[placeholders['adm2_subarea']],
description='or of second level:',
style=style,
layout=dropdown_layout
)
# Dropdown for timescales
timescale_selector = widgets.Dropdown(
options=[placeholders['timescale']] + list(timescales.keys()),
description='Select a timescale:',
style=style,
layout=dropdown_layout
)
# Dropdown for months
month_selector = widgets.Dropdown(
options=[placeholders['month']] + list(months.keys()),
description='Select a month:',
style=style,
layout=dropdown_layout
)
# Dropdown for years
current_year = datetime.datetime.now().year
years_options = [str(year) for year in range(1940, current_year + 1)]
year_selector = widgets.Dropdown(
options=[placeholders['year']] + years_options,
description='Select a year:',
disabled=False,
style=style,
layout=dropdown_layout
)
# SelectionRangeSlider for years
year_range_selector = widgets.SelectionRangeSlider(
options=years_options,
index=(len(years_options) - 1, len(years_options) - 1), # Start and end at the last
description='Select the year range:',
disabled=False,
style=style,
layout=range_layout
)
selectors = {
"country" : country_selector,
"adm1_subarea": adm1_subarea_selector,
"adm2_subarea": adm2_subarea_selector,
"timescale": timescale_selector,
"month": month_selector,
"year": year_selector,
"year_range": year_range_selector
}
month_widgets_btn = widgets.Button(
description='Get data',
disabled=False,
button_style='info', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Click me',
icon='filter', # (FontAwesome names without the `fa-` prefix)month
layout=btn_layout
)
month_widgets_btn.custom_name='month_widgets_btn'
year_widgets_btn = widgets.Button(
description='Get data',
disabled=False,
button_style='info', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Click me',
icon='filter', # (FontAwesome names without the `fa-` prefix)
layout=btn_layout
)
year_widgets_btn.custom_name='year_widgets_btn'
year_range_widgets_btn = widgets.Button(
description='Get data',
disabled=False,
button_style='info', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Click me',
icon='filter', # (FontAwesome names without the `fa-` prefix)
layout=btn_layout
)
year_range_widgets_btn.custom_name='year_range_widgets_btn'
# Output area for display updates
output_area = widgets.Output()
def setup_observers():
"""
Sets up observers for UI widgets to handle interactions and updates dynamically in a graphical user interface.
This function ensures that observers are only set once using a function attribute to track whether observers have
already been established, enhancing efficiency and preventing multiple bindings to the same event.
Observer is attached to widgets for country selection. This observer triggers specific functions when the 'value' property
of the widgets changes, facilitating responsive updates to the user interface
based on user interactions.
Notes:
- This function uses a custom attribute `observers_set` on itself to ensure observers are set only once.
"""
if not hasattr(setup_observers, 'observers_set'):
# When 'value' changes, update_subareas function will be called to update the dropdown menus
# Create a partial function that includes the additional parameters
country_selector.observe(partial(uti.update_subareas,
country_list=country_list,
placeholders=placeholders,
adm1_subarea_selector=adm1_subarea_selector,
adm2_subarea_selector=adm2_subarea_selector), 'value')
# Set a flag to indicate observers are set
setup_observers.observers_set = True
def update_and_get_data(btn_name):
"""
Update and retrieve data based on user interactions and selections.
This function handles user interactions, validates selections, calculates geographic bounding boxes,
fetches the corresponding data subset, and updates the output area with relevant information and a map display.
Parameters:
btn_name (str): The name of the button that triggered the interaction.
Global Variables:
selected (dict): Dictionary containing current selections for various parameters.
placeholders (dict): Dictionary of placeholder values.
output_area (OutputArea): The output area widget to display messages and results.
subset_data (xarray.DataArray): Subset of data fetched based on the bounding box.
index (str): Index for the subset data, constructed from timescale value.
bounding_box (tuple): Bounding box coordinates (min_lon, min_lat, max_lon, max_lat) for the selected area.
active_btn (str): The name of the currently active button.
Steps:
1. Set the active button name.
2. Update the month and year selections based on the button interaction.
3. Validate the current selections.
4. If selections are valid:
a. Clear the output area.
b. Retrieve the geographic boundaries for the selected area.
c. Calculate the bounding box for the selected area.
d. Fetch the data subset based on the bounding box.
e. Determine the administrative level, selected area name, timescale, and time period.
f. Print information about the uploaded subset data.
g. Display the map with the bounding box and appropriate zoom level.
Notes:
- The function assumes the existence of utility functions within the 'uti' module for handling interactions, validations,
data fetching, and map display.
- The global variables should be properly initialized before calling this function.
"""
global selected, placeholders, output_area, subset_data, index, bounding_box, active_btn
map_display = None
active_btn = btn_name
uti.month_year_interaction(btn_name, month_selector, year_selector, selected, placeholders)
if uti.validate_selections(btn_name, selected, selectors, placeholders, output_area):
with output_area:
output_area.clear_output(wait=True)
coordinates = uti.get_boundaries(selected, country_list, placeholders)
# print(coordinates)
bounding_box = uti.calculate_bounding_box(coordinates)
# print(bounding_box)
# sample_coordinates = coordinates[:3] # Showing first 3 coordinates for brevity
# print('Original Coordinates Sample: ', sample_coordinates)
# print('Bounding Box: ', bounding_box)
# Fetching data using the bounding box
subset_data = uti.get_xarray_data(btn_name, bounding_box, selectors, placeholders, months, timescales)
index = f"SPEI{timescales[selectors['timescale'].value]}"
adm_level, selected_area = uti.get_adm_level_and_area_name(selected, placeholders)
timescale = selected['timescale']
time_period = uti.get_period_of_time(btn_name, selected, placeholders)
print(f"SPEI subset data uploaded for {selected_area}, administrative level {adm_level}, timescale {timescale}, period {time_period}")
zoom_start = 4
if adm_level == 'ADM1' or adm_level == 'ADM2':
zoom_start = 8
map_display = uti.display_map(bounding_box, zoom_start)
map_iframe = uti.display_map_in_iframe(map_display)
display(map_iframe)
# Set up widget interaction
def on_button_clicked(btn):
update_and_get_data(btn.custom_name)
# Setup observers
setup_observers()
# Update existing selectors
previous_selection = uti.read_json_to_dict('selection.json')
# Set up widgets with previous settings
country_selector.value = previous_selection.get('country', placeholders['country'])
adm1_subarea_selector.value = previous_selection.get('adm1_subarea', placeholders['adm1_subarea'])
adm2_subarea_selector.value = previous_selection.get('adm2_subarea', placeholders['adm2_subarea'])
timescale_selector.value = previous_selection.get('timescale', placeholders['timescale'])
month_selector.value = previous_selection.get('month', placeholders['month'])
month_widgets_btn.on_click(on_button_clicked)
# Display widgets
display(country_selector, adm1_subarea_selector, adm2_subarea_selector, timescale_selector, month_selector, month_widgets_btn, output_area)
uti.display_data_details(active_btn, selected, subset_data[index])
Country: Italy ADM1 subarea: no adm1 subarea selected... ADM2 subarea: no adm2 subarea selected... Month: March Timescale: 3 months Time values in the subset: 84 Latitude values in the subset: 47 Longitude values in the subset: 48 Data sample: [[ 0.10656517 -0.03892938 -0.21370953 -0.26820409 -0.31034104] [ 0.20403971 0.12018371 -0.05364322 -0.16029305 -0.19767122] [ 0.09961821 0.06639799 0.11098377 0.02562103 0.10002738] [-0.0285368 -0.03788812 0.03719397 0.16075343 0.21798218] [-0.13031167 -0.14410005 -0.00440113 0.11054687 0.14219684]]
processed_subset, change_summary = uti.process_datarray(subset_data[index])
print(processed_subset, '\n')
print('Change summary:')
for key, val in change_summary.items():
print(key, val)
<xarray.DataArray 'SPEI3' (time: 84, lat: 47, lon: 48)>
dask.array<where, shape=(84, 47, 48), dtype=float64, chunksize=(1, 47, 48), chunktype=numpy.ndarray>
Coordinates:
* time (time) datetime64[ns] 1940-03-01T06:00:00 ... 2023-03-01T06:00:00
* lon (lon) float64 6.75 7.0 7.25 7.5 7.75 ... 17.5 17.75 18.0 18.25 18.5
* lat (lat) float64 35.5 35.75 36.0 36.25 36.5 ... 46.25 46.5 46.75 47.0
Attributes:
long_name: Standardized Drought Index (SPEI3)
units: -
Change summary:
invalid_values_replaced 86268
invalid_ratio 0.45523049645390073
duplicates_removed 0
cftime_conversions 0
stat_values = uti.compute_stats(processed_subset)
uti.create_scatterplot(stat_values, timescales, selected, placeholders)
uti.create_boxplot(stat_values, timescales, selected, placeholders)
# Update existing selectors
previous_selection = uti.read_json_to_dict('selection.json')
# Set up widgets with previous settings
country_selector.value = previous_selection.get('country', placeholders['country'])
adm1_subarea_selector.value = previous_selection.get('adm1_subarea', placeholders['adm1_subarea'])
adm2_subarea_selector.value = previous_selection.get('adm2_subarea', placeholders['adm2_subarea'])
timescale_selector.value = previous_selection.get('timescale', placeholders['timescale'])
year_selector.value = previous_selection.get('year', placeholders['year'])
year_widgets_btn.on_click(on_button_clicked)
# Display widgets
display(country_selector, adm1_subarea_selector, adm2_subarea_selector, timescale_selector, year_selector, year_widgets_btn, output_area)
uti.display_data_details(active_btn, selected, subset_data[index])
Country: Italy ADM1 subarea: Nord-Est ADM2 subarea: no adm2 subarea selected... Year: 1998 Timescale: 3 months Time values in the subset: 12 Latitude values in the subset: 14 Longitude values in the subset: 20 Data sample: [[-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 -9.99900000e+03 6.93787587e-01] [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 7.10923387e-01 6.71962754e-01] [ 7.81756359e-01 7.88598743e-01 6.59171139e-01 6.02512805e-01 5.53822168e-01] [ 6.79895365e-01 6.37505279e-01 4.48464948e-01 3.38852353e-01 2.47576329e-01] [ 4.91366561e-01 4.10632395e-01 2.86515031e-01 2.66845584e-01 1.80545017e-01]]
processed_subset, change_summary = uti.process_datarray(subset_data[index])
print(processed_subset, '\n')
print('Change summary:')
for key, val in change_summary.items():
print(key, val)
<xarray.DataArray 'SPEI3' (time: 12, lat: 14, lon: 20)>
dask.array<where, shape=(12, 14, 20), dtype=float64, chunksize=(1, 14, 20), chunktype=numpy.ndarray>
Coordinates:
* time (time) datetime64[ns] 1998-01-01T06:00:00 ... 1998-12-01T06:00:00
* lon (lon) float64 9.25 9.5 9.75 10.0 10.25 ... 13.25 13.5 13.75 14.0
* lat (lat) float64 43.75 44.0 44.25 44.5 44.75 ... 46.25 46.5 46.75 47.0
Attributes:
long_name: Standardized Drought Index (SPEI3)
units: -
Change summary:
invalid_values_replaced 444
invalid_ratio 0.13214285714285715
duplicates_removed 0
cftime_conversions 0
stat_values = uti.compute_stats(processed_subset, full_stats=False)
uti.create_linechart(stat_values, timescales, selected, placeholders)
# Update existing selectors
previous_selection = uti.read_json_to_dict('selection.json')
# Set up widgets with previous settings
country_selector.value = previous_selection.get('country', placeholders['country'])
adm1_subarea_selector.value = previous_selection.get('adm1_subarea', placeholders['adm1_subarea'])
adm2_subarea_selector.value = previous_selection.get('adm2_subarea', placeholders['adm2_subarea'])
timescale_selector.value = previous_selection.get('timescale', placeholders['timescale'])
year_range_selector.value = previous_selection.get('year_range')
year_range_widgets_btn.on_click(on_button_clicked)
# Display widgets
display(country_selector, adm1_subarea_selector, adm2_subarea_selector, timescale_selector, year_range_selector, year_range_widgets_btn, output_area)
uti.display_data_details(active_btn, selected, subset_data[index])
Country: Italy
ADM1 subarea: Nord-Est
ADM2 subarea: no adm2 subarea selected...
Year range: ('2014', '2024')
Timescale: 3 months
Time values in the subset: 124
Latitude values in the subset: 14
Longitude values in the subset: 20
Data sample: [[-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 -9.99900000e+03
9.25071009e-01]
[-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 1.02011684e+00
1.01443264e+00]
[ 1.21878881e+00 1.29934835e+00 1.21117076e+00 1.21989841e+00
1.21718637e+00]
[ 1.27909222e+00 1.27673982e+00 1.20574073e+00 1.14198875e+00
1.07900212e+00]
[ 1.20232192e+00 1.17460068e+00 1.07761040e+00 1.18870283e+00
1.17344467e+00]]
processed_subset, change_summary = uti.process_datarray(subset_data[index])
print(processed_subset, '\n')
print('Change summary:')
for key, val in change_summary.items():
print(key, val)
<xarray.DataArray 'SPEI3' (time: 122, lat: 14, lon: 20)>
dask.array<getitem, shape=(122, 14, 20), dtype=float64, chunksize=(1, 14, 20), chunktype=numpy.ndarray>
Coordinates:
* time (time) datetime64[ns] 2014-01-01T06:00:00 ... 2024-02-01T06:00:00
* lon (lon) float64 9.25 9.5 9.75 10.0 10.25 ... 13.25 13.5 13.75 14.0
* lat (lat) float64 43.75 44.0 44.25 44.5 44.75 ... 46.25 46.5 46.75 47.0
Attributes:
long_name: Standardized Drought Index (SPEI3)
units: -
Change summary:
invalid_values_replaced 4588
invalid_ratio 0.13214285714285715
duplicates_removed 2
cftime_conversions 4
stat_values = uti.compute_stats(processed_subset, full_stats=False)
uti.create_stripechart(stat_values, timescales, selected, placeholders)
uti.create_stripechart(stat_values, timescales, selected, placeholders, 'year')
countries' boundaries source: www.geoboundaries.org